# -*- coding: utf-8 -*-
#!/usr/bin/env python

import logging
import sys
import math
import string
import threading

from datetime import datetime, timedelta
from utils import *
from google.appengine.api import memcache
from google.appengine.ext import ndb
from google.appengine.ext.blobstore import BlobInfo
from app_global import *

class AssertionError(Exception):
	def __init__(self, value):
		self.value = value
	def __str__(self):
		return repr(self.value)

class RootedModel(ndb.Model):
	@classmethod
	def ancestorKey(cls):
		ndb.Key(cls, cls.__name__)

class UserModel(ndb.Model):
	@classmethod
	def ancestorKey(cls, user):
		return user.key

	@classmethod
	def allRecords(cls, user):
		return cls.query(cls.user_id==user.key.id(), ancestor=user.key)

###############################################################################
class Platform(RootedModel): # 1:IOS, 2:PlayStore
	name = ndb.StringProperty(required=True)
	version = ndb.IntegerProperty('version',default=0)
	url = ndb.StringProperty(required=True)

class Asset(RootedModel):
	platform = ndb.IntegerProperty(required=True)
	version = ndb.IntegerProperty(default=0)
	name = ndb.StringProperty(required=True)
	blob_key = ndb.BlobKeyProperty(default=None)
	created_at = ndb.DateTimeProperty(auto_now_add=True)
	
	@classmethod
	def getPatches(cls, version, platform):
		qry = Asset.query(ancestor=Asset.ancestorKey())
		assets = qry.filter(Asset.platform==platform, Asset.version>version).fetch()
		objects = []
		blobKeys = []
		for asset in assets:
			for obj in objects:
				if obj["name"]==asset.name and obj["version"]<asset.version:
					objects.remove(obj)
					if obj["key"]:
						blobKeys.remove(obj["key"])
					break
			objects.append(
				{
					"name":asset.name,
					"url":("%s/download/asset/%s" % (DOMAIN, asset.blob_key,)) if asset.blob_key else None,
					"version":asset.version,
					"key":asset.blob_key,
				}
			)
			if asset.blob_key:
				blobKeys.append(asset.blob_key)
		
		for obj in objects:
			del obj["version"]
			del obj["key"]
		
		return sum([bi.size for bi in BlobInfo.get(blobKeys)]), objects
	
class Notice(RootedModel):
	visible = ndb.BooleanProperty(default=True)
	url = ndb.StringProperty(required=True)
	created_at = ndb.DateTimeProperty(auto_now_add=True)
	updated_at = ndb.DateTimeProperty(auto_now=True)

class Season(RootedModel):
	count = ndb.IntegerProperty(required=True)
	expired_at = ndb.DateTimeProperty(required=True)
	updated_at = ndb.DateTimeProperty(auto_now=True)
	
	@classmethod
	def season(cls):
		season = memcache.get("season")
		if season is None:
			season = cls.get_by_id(1,ancestor=cls.ancestorKey())
			memcache.set("season", season)
		return season
	
	@classmethod
	def incr(cls):
		season = None
		while True:
			try:
				season = cls.get_by_id(1,ancestor=cls.ancestorKey())
				season.count += 1
				season.expired_at = season.expired_at + timedelta(days=7)
				season.put()
				break
			except:
				continue
		while True:
			try:
				memcache.set("season", season)
				break
			except:
				continue
		return season
	
	def weeklyExpireInSec(self, now):
		insec = int(math.ceil((self.expired_at - now).total_seconds()))
		return insec

class DailyActiveUser(ndb.Model):
	platform_id = ndb.IntegerProperty(required=True)
	
	@classmethod
	def exists(cls, user):
		return cls.get_by_id(user.key.id()) is not None
	
	@classmethod
	def append(cls, user):
		cls(id=user.key.id(), platform_id=user.platform_id).put()

class UpdatedPushToken(ndb.Model):
	platform_id = ndb.IntegerProperty(required=True)
	push_token = ndb.StringProperty(default=None)
	application_version = ndb.IntegerProperty(default=0)
	asset_version = ndb.IntegerProperty(default=0)
	invalid = ndb.BooleanProperty(default=False)
	updated_at = ndb.DateTimeProperty(auto_now=True)

class MissionRecord(ndb.Model):
	daily_season = ndb.IntegerProperty(default=1)
	weekly_season = ndb.IntegerProperty(default=1)
	# daily
	dplays = ndb.IntegerProperty(repeated=True)
	dbuff = ndb.IntegerProperty(default=0)
	dfeeds = ndb.IntegerProperty(repeated=True)
	# weekly
	wplays = ndb.IntegerProperty(repeated=True)
	wbuff = ndb.IntegerProperty(default=0)
	wfeeds = ndb.IntegerProperty(repeated=True)
	
	daily_statuses = ndb.IntegerProperty(repeated=True)	# -1:미달성, 0:완료(확인), 1:달성(미확인)
	weekly_statuses = ndb.IntegerProperty(repeated=True)
	
	def initializeDaily(self, season):
		self.daily_season = season
		self.dplays = [0]*GAME_COUNT
		self.dbuff = 0
		self.dfeeds = [0]*FEED_SLOTS
		self.daily_statuses = [-1]*3

	def initializeWeekly(self, season):
		self.weekly_season = season
		self.wplays = [0]*GAME_COUNT
		self.wbuff = 0
		self.wfeeds = [0]*FEED_SLOTS
		self.weekly_statuses = [-1]*3
		
	def initializeAll(self, mission):
		self.initializeDaily(mission.daily_season)
		self.initializeWeekly(mission.weekly_season)
		
class AccrueRecord(ndb.Model):
	play = ndb.IntegerProperty(default=0)
	best_score = ndb.IntegerProperty(default=0)
	rank_ups = ndb.IntegerProperty(repeated=True)
	invite = ndb.IntegerProperty(default=0)
	mission_completed = ndb.IntegerProperty(default=0)
	present = ndb.IntegerProperty(default=0) # 선물(복숭아) 받기 횟수 
	buy_character = ndb.IntegerProperty(default=0)
	max_character_level = ndb.IntegerProperty(default=1)
	character_action = ndb.IntegerProperty(default=0) # 캐릭터 호감도 행동 횟수
	character_buff = ndb.IntegerProperty(default=0) # 캐릭터 호감도 100% 횟수
	achieve_rewards = ndb.IntegerProperty(repeated=True) # achievements 에서, 보상이 나갔던 것들의 ID를 저장함

class SeasonRecord(ndb.Model):
	season = ndb.IntegerProperty(default=1)
	rewarded = ndb.BooleanProperty(default=False)
	scores = ndb.IntegerProperty(repeated=True)
	ranks = ndb.IntegerProperty(repeated=True)
	
class User(ndb.Model):	
	platform_id = ndb.IntegerProperty(required=True)
	application_version = ndb.IntegerProperty(required=True)
	asset_version = ndb.IntegerProperty(required=True)
	name = ndb.StringProperty(default=None)
	image_url = ndb.StringProperty(default=None)
	push_token = ndb.StringProperty(default=None)
	cash = ndb.IntegerProperty(default=0)
	coin = ndb.IntegerProperty(default=5000)
	bout = ndb.IntegerProperty(default=5)	
	push_blocked = ndb.BooleanProperty(default=False)
	bout_blocked = ndb.BooleanProperty(default=False)
	invite_rewards = ndb.BooleanProperty(repeated=True)	
	accrue_record = ndb.StructuredProperty(AccrueRecord,default=AccrueRecord())
	mission_record = ndb.StructuredProperty(MissionRecord, default=MissionRecord())
	bout_feed_at = ndb.DateTimeProperty(auto_now_add=True)		
	"""
	sesaon1 and season2 is toggled data group, if one is current, another is previous
	"""
	season1 = ndb.StructuredProperty(SeasonRecord,default=SeasonRecord())
	season2 = ndb.StructuredProperty(SeasonRecord,default=SeasonRecord())
	"""
	"""
	tutorials = ndb.IntegerProperty(repeated=True)
	season = ndb.IntegerProperty(required=True)
	daily_stamp = ndb.IntegerProperty(default=0)
	loggedin_at = ndb.DateTimeProperty()
	session_stamp = ndb.IntegerProperty(default=0)
	review = ndb.BooleanProperty(default=False)
	event_record = ndb.JsonProperty(default=[])
	author_key = ndb.IntegerProperty()
	game_result_key = ndb.IntegerProperty()	
	updated_at = ndb.DateTimeProperty(auto_now=True)
	created_at = ndb.DateTimeProperty(auto_now_add=True)
	
	def __init__(self, **kwargs):
		super(User, self).__init__(**kwargs)
	
	def availableItems(self, game, items):
		needCoin = 0
		for i in range(4):
			if items[i]>0:
				needCoin += ITEM_COINS[i]
		if self.coin<needCoin: # 코인부족하면, 모든 아이템 장착해제(가차로 뽑은건 예외)
			for i in range(4):
				items[i] = 0
			needCoin = 0
		
		return needCoin, [i for i in items]

	def seasonUpdated(self, season=None):
		if season is None:
			season = Season.season()
		prevSeason = season.count - 1
		if prevSeason==0:
			return False
		data = self.season1 if (prevSeason % 2 == 0) else self.season2
		return data.rewarded==False and sum(data.scores)>0 and data.season == prevSeason
	
	def initSeason(self, data, season):
		data.season = season
		data.scores = [0] * GAME_COUNT
		data.ranks = [0] * GAME_COUNT
		data.rewarded = False
	
	def prevSeasonData(self, season=None):
		if season is None:
			season = Season.season().count
		prev = season - 1
		if prev==0: # during first season
			return None
		if prev % 2 == 0 and prev==self.season1.season: # exists prev season data
			return self.season1
		if prev % 2 == 1 and prev==self.season2.season: # exists prev season data
			return self.season2
		return None
	
	def seasonData(self, season=None):
		if season is None:
			season = Season.season().count
		if season % 2 == 0:
			if season!=self.season1.season:
				self.initSeason(self.season1, season)
			return self.season1
		else:
			if season!=self.season2.season:
				self.initSeason(self.season2, season)
			return self.season2
								
	def remainiInvite(self, invite):
		keys = INVITE_REWARDS.keys()
		keys.sort()
		target = 0
		for k in keys:
			if k>target and target==0:
				target = k
			elif k>target:
				target = min(target, k)
		return max(0, target-invite)
				
	def feedBoutInSec(self):
		if self.bout >= MAX_FEED_BOUT:
			return 0
		return max(1,INTERVAL_FEED_BOUT - int(math.ceil((datetime.now() - self.bout_feed_at).total_seconds())))	
		
class UserFriendBout(UserModel):
	user_id = ndb.IntegerProperty(indexed=True, required=True)
	friend_id = ndb.IntegerProperty(required=True)
	requested_at = ndb.DateTimeProperty()
	sent_at = ndb.DateTimeProperty()
	
	def boutAcceptableInSec(self, now):
		if self.sent_at:
			return max(0,INTERVAL_SEND_BOUT - int(math.ceil((now - self.sent_at).total_seconds())))
		return 0
		
	def boutRequestableInSec(self, now):
		if self.requested_at:
			return max(0,INTERVAL_REQUEST_BOUT - int(math.ceil((now - self.requested_at).total_seconds())))
		return 0
		
class UserInbox(UserModel):
	user_id = ndb.IntegerProperty(indexed=True, required=True)
	message = ndb.StringProperty()
	reward = ndb.IntegerProperty()
	amount = ndb.IntegerProperty()
	flag = ndb.IntegerProperty(default=0)
	created_at = ndb.DateTimeProperty(auto_now_add=True)

	def serialize(self):
		return {
			"id":self.key.id(),
			"message":self.message,
			"reward":self.reward,
			"amount":self.amount,
			"created_at":self.created_at
		}

	@classmethod
	def deleteAll(cls, user, inboxes):
		ndb.delete_multi([i.key for i in inboxes])
		memcache.set('%s:newInbox' % user.key.id(), 0)
		
	@classmethod
	def delete(cls, user, inbox):
		newInbox = memcache.get('%s:newInbox' % user.key.id())
		if newInbox:
			newInbox -= 1
			memcache.set('%s:newInbox' % user.key.id(), newInbox)
		inbox.key.delete()
	
	@classmethod
	def unreadCount(cls, user):
		newInbox = memcache.get('%s:newInbox' % user.key.id())
		if newInbox:
			return newInbox
		
		records = cls.query(cls.user_id==user.key.id(), ancestor=cls.ancestorKey(user)).fetch()
		count = len(records)
		if count>50:
			keys = []
			for inbox in records[50:]:
				keys.append(inbox.key)
			ndb.delete_multi(keys)
			records = records[:50]
		memcache.set('%s:newInbox' % user.key.id(), count)
		return count

class UserRestricted(UserModel):
	user_id = ndb.IntegerProperty(indexed=True, required=True)
	message = ndb.StringProperty(required=True)
	created_at = ndb.DateTimeProperty(auto_now_add=True)
	expired_at = ndb.DateTimeProperty(required=True)

class UserPurchase(UserModel):
	user_id = ndb.IntegerProperty(indexed=True, required=True)
	cash_id = ndb.IntegerProperty()
	cash = ndb.IntegerProperty()
	status = ndb.IntegerProperty()
	receipt = ndb.BlobProperty()
	signature = ndb.BlobProperty()
	created_at = ndb.DateTimeProperty(auto_now_add=True)
	updated_at = ndb.DateTimeProperty(auto_now=True)

class EventConfig(RootedModel):
	visible = ndb.BooleanProperty(default=True)
	title = ndb.StringProperty(required=True)
	message = ndb.StringProperty(required=True)
	trigger = ndb.IntegerProperty(required=True)
	count = ndb.IntegerProperty(default=1)
	start_at = ndb.DateTimeProperty()
	end_at = ndb.DateTimeProperty()
	reward_type = ndb.IntegerProperty(required=True)
	reward_data = ndb.IntegerProperty(required=True)

class ReservedPush(RootedModel):
	platform_id = ndb.IntegerProperty(required=True)
	application_version = ndb.IntegerProperty(default=-1)
	asset_version = ndb.IntegerProperty(default=-1)
	user_ids = ndb.IntegerProperty(repeated=True)
	reserved_at = ndb.DateTimeProperty(default=None)
	deadline_at = ndb.DateTimeProperty(default=None)
	title = ndb.StringProperty()
	message = ndb.StringProperty(required=True)
	badge = ndb.IntegerProperty(default=0)
	step = ndb.IntegerProperty(default=1) # 0:completed, 
	count = ndb.IntegerProperty(default=0)
	created_at = ndb.DateTimeProperty(auto_now_add=True)

class Mission(RootedModel):
	daily_season = ndb.IntegerProperty(default=1)
	weekly_season = ndb.IntegerProperty(default=1)
	daily_missions = ndb.IntegerProperty(repeated=True) # 3개
	weekly_missions = ndb.IntegerProperty(repeated=True) # 3개
	weekly_expired_at = ndb.DateTimeProperty(required=True)
	
	@classmethod
	def todayMission(cls):
		tms = memcache.get("todayMission")
		if not tms:
			tms = Mission.query().get()
			memcache.set("todayMission", tms)
		return tms
		
	def weeklyExpireInSec(self, now):
		now = replace_timezone(now, 'Asia/Seoul')
		insec = int(math.ceil((self.weekly_expired_at - now).total_seconds()))
		return insec
	
	def dailyExpireInSec(self, now):
		now = replace_timezone(now, 'Asia/Seoul')
		tomorow = now+timedelta(days=1)
		tomorow = datetime(tomorow.year, tomorow.month, tomorow.day)
		insec = int(math.ceil((tomorow-now).total_seconds()))
		return insec
		
class UserInvite(UserModel):
	user_id = ndb.IntegerProperty(indexed=True, required=True)
	friend_id = ndb.IntegerProperty(required=True)
	updated_at = ndb.DateTimeProperty(auto_now=True)
	
	def inviteAcceptableInSec(self):
		return max(0,INTERVAL_INVITE - int(math.ceil((datetime.now() - self.updated_at).total_seconds())))

class UserLog(UserModel):
	user_id = ndb.IntegerProperty(indexed=True, required=True)
	action = ndb.IntegerProperty(required=True)
	log = ndb.JsonProperty(required=True)
	created_at = ndb.DateTimeProperty(auto_now_add=True)
	
	@classmethod
	def append(cls, user, action, log):
		try:
			cls(parent=user.key, user_id=user.key.id(), action=action, log=json.dumps(log)).put()
		except:
			pass
	
	@classmethod
	def appendJson(cls, user, action, jsonlog):
		try:
			cls(parent=user.key, user_id=user.key.id(), action=action, log=jsonlog).put()
		except:
			pass

class Abuser(ndb.Model):
	network_hack = ndb.IntegerProperty(default=0)
	memory_hack = ndb.IntegerProperty(default=0)
	binary_hack = ndb.IntegerProperty(default=0)
	device_hack = ndb.IntegerProperty(default=0)
	iap_hack = ndb.IntegerProperty(default=0)
	iap_fail = ndb.IntegerProperty(default=0)
	total = ndb.IntegerProperty(default=0)
	
	@classmethod
	def checkRestrict(cls, userId, userAbuse):
		# TODO : policy for abuser
		return
	
	@classmethod
	def addUserId(cls, userId, kind):
		ua = cls.get_by_id(userId)
		if ua is None:
			ua = cls(id=userId)
		
		if kind==ABUSE_NETWORK_HACK:
			ua.network_hack += 1
		elif kind==ABUSE_MEMORY_HACK:
			ua.memory_hack += 1
		elif kind==ABUSE_BINARY_HACK:
			ua.binary_hack += 1
		elif kind==ABUSE_DEVICE_HACK:
			ua.device_hack += 1
		elif kind==ABUSE_IAP_HACK:
			ua.iap_hack += 1
		elif kind==ABUSE_IAP_FAIL:
			ua.iap_fail += 1
			
		if kind!=ABUSE_IAP_FAIL:
			ua.total += 1
		
		cls.checkRestrict(userId, ua)
		ua.put()
	
	@classmethod
	def addUser(cls, user, kind):
		cls.addUserId(user.key.id(), kind)

